前一篇介紹了函式與變數,那麼接下來要來回頭補充一下關於前面所沒聊到的變數作用域。
首先先讓我們回顧一下 JavaScript 的變數作用域部分,在 JavaScript 中有三種宣告變數的方式,分別為:
var
let
const
我們都知道這三種宣告變數的方式與它所屬的變數作用域都是不同的,例如: var
的作用域是以函式 (function
) 作為區分,因此如果不是在函式內宣告的話,就很容易影響到全域環境,也就是所謂的全域污染:
if(true) {
var myName = 'Ray';
}
console.log(myName); // Ray
function fn () {
var qq = 'Ray';
}
fn();
console.log(qq); // Uncaught ReferenceError: qq is not defined
那麼全域污染會有什麼下場呢?最明顯就是不小心變數互相覆蓋的問題:
var myName = 'Ray';
if(true) {
var myName = 'qq';
}
console.log(myName); // qq
而 let
與 const
則是以區塊 (block) 作為區分,什麼是區塊呢?只要你看到 {
、 }
包起來的就是一個區塊、一個 block:
if(true) {
let myName = 'Ray';
}
console.log(myName); // Uncaught ReferenceError: myName is not defined
因此當我們在閱讀程式碼時,我們是可以透過宣告變數的關鍵字來了解該變數是屬於哪一種變數作用域,那 Python 呢?Python 又如何呢?我們可以先試著將上面程式碼改成 Python 版本試試看:
if True:
myName = 'Ray'
print('myName:', myName) # myName: Ray
首先我們可以發現 Python 並不是以 Block 作為變數作用域的,那麼會不會是以函式當作作用域呢?這邊我們也可以實際驗證一下其結果:
def fn():
myName = 'Ray'
fn()
print('myName:', myName) # NameError: name 'myName' is not defined
答案是...
是的,Python 在變數宣告上是以 function
為作用域。
透過上面的範例我們可以了解到 Python 的變數類似於 JavaScript 的 var
變數而不是 let
變數,但這邊要注意 Python 並沒有提升的概念唷。
因此我們也可以知道一個不小心是有可能發生污染事件的:
myName = 'Ray'
if True:
myName = 'qq'
print(myName) # qq
接下來是關於範圍鍊的部分,JavaScript 有一個觀念是關於範圍鍊,那麼什麼是範圍鏈呢?簡單來講就是,假使這個變數不存在於這個函式時,它會往外層去尋找這個變數或是函式是否存在,而且是一直一直一直往上尋找:
var myName = 'Ray';
function fn1() {
function fn2() {
function fn3() {
console.log('myName:', myName); // myName: Ray
}
fn3();
}
fn2();
}
fn1();
又或者是這一種類型的範例程式碼:
var value = 1; // 全域變數
function fu1() {
console.log(value); // 1
}
function fu2() {
var value = 2; // 區域變數
fu1();
}
fu2();
因此當函式本身若沒有相對應的變數或函式時,它就會自動往外層去尋找,而這就是 JavaScript 所謂的範圍鍊概念,如果對於範圍鍊不熟悉的話,可詳見 此篇 文章。
現在就讓我們拉回到 Python,Python 也會有相同概念嗎?我們先將前面其中一個範例程式碼直接改寫成 Python 來試試看:
value = 1
def fu1():
print(value) # 1
def fu2():
value = 2
fu1()
fu2()
從結果論來講,我們可以看到 print
輸出的結果依然是 1
而不是找不到這個變數,因此 Python 在變數的尋找上是與 JavaScript 非常相像的,所以你要說 Python 有沒有範圍鍊得概念呢?我想是有的。
最後的結尾處我想聊一下關於 nonlocal
與 global
這兩個很特別的東西,首先先讓我們看一小段 Python 範例,也就是非常巢狀的巢狀函式:
def fn1():
def fn2():
def fn3():
而我們在實際開發時單字量可能很少,例如我這個英文很差的人永遠都只有那幾個單字
回歸到 Python 的部分,剛剛有提到往往我們開發系統時可能會寫得很長很多,所以變數名稱就會有很高的機率重複,例如就像這樣:
a = 'Ray'
def fn1():
a = 'AA'
def fn2():
a = 'BB'
def fn3():
a = 'CC'
雖然以結論來講上面程式碼看起來是沒有什麼問題的,畢竟每一個函式都有一個自己的變數,但是如果今天我們希望 fn3
的 a
是重新賦予 global 的變數 a
呢?
a = 'Ray'
def fn1():
a = 'AA'
print('fn1', a) # AA
def fn2():
a = 'BB'
print('fn2', a) # BB
def fn3():
a = 'CC'
print('fn3', a) # CC
fn3()
fn2()
fn1()
print('global', a) # Ray
發現了嗎?由於我們在每個函式內重新宣告了一個變數都是 a
,所以 Python 認為我們是要宣告一個新的變數而不是賦予到 global a
中,因此這時候我們就可以使用 Python 所提供的 global 變數名稱
語法告知 Python:「嘿!我這個變數是 Global 的哦!你不要建立一個區域變數!」:
a = 'Ray'
def fn1():
a = 'AA'
print('fn1', a) # AA
def fn2():
a = 'BB'
print('fn2', a) # BB
def fn3():
global a
a = 'CC'
print('fn3', a) # CC
fn3()
fn2()
fn1()
print('global', a) # 被 fn3 重新賦予導致覆蓋成 CC
那麼這樣就可以解決我們作用域的問題,畢竟就如同前面所言 Python 並沒有變數宣告的語法,因此 Python 必須透過其他的方式去告知它這個變數不是重新定義,而是直接取用外層。
反之如果你想取用的是函式內的變數,那麼也是一樣,只是在此所使用的是 nonlocal
:
a = 'Ray'
def fn1():
a = 'AA'
print('fn1', a) # fn1:AA
def fn2():
a = 'BB'
print('fn2 取代前:', a) # fn2 取代前:BB
def fn3():
nonlocal a
a = 'CC'
print('fn3', a)
fn3()
print('fn2 取代前:', a) # 被更改成 CC
fn2()
fn1()
print('global', a) # Global Ray
那麼這時候你可能會想說為什麼 fn1
的 a
不會受到影響呢?你可以利用範圍鍊的概念去思考,當 fn3
的 a
像外層尋找時,會優先找到 fn2
的 a
因此它就會停止向上了,所以假設如果 fn2
的 a 若不存在的話,fn3
的 a
變數就會一直往上尋找,這一點觀點我們也可以嘗試驗證:
a = 'Ray'
def fn1():
a = 'AA'
print('fn1 取代前:', a) # fn1:AA
def fn2():
def fn3():
nonlocal a
a = 'CC'
print('fn3', a) # CC
fn3()
fn2()
print('fn1 取代後:', a) # fn1:CC
fn1()
print('global', a) # Global Ray
透過一個實際的案例來講,我們也可以再次體會到 JavaScript 的範圍鍊觀念是可以套用在 Python 上來看的,不知道有沒有讓已經快燒壞大腦的你有一點明瞭了呢?
昨天調整了花雕醉雞的比例,基本上大約 200cc 花雕酒配 100cc 米酒差不多,然後這次嘗試直接將鹽包著肌肉一起處理,發現晚上回到家弄來吃之後還不錯吃。
補充一下,Python 變數作用域: LEGB Rule
https://realpython.com/python-scope-legb-rule/
感謝補充 :D